from copy import deepcopy

from .Constants import PY2
from .Elements import *

if PY2:
    StringType = basestring
else:
    StringType = str

DEFAULT_TAB_WIDTH = 720

ParagraphAlignmentMap = {
    ParagraphPropertySet.LEFT: "ql",
    ParagraphPropertySet.RIGHT: "qr",
    ParagraphPropertySet.CENTER: "qc",
    ParagraphPropertySet.JUSTIFY: "qj",
    ParagraphPropertySet.DISTRIBUTE: "qd",
}

TabAlignmentMap = {
    TabPropertySet.LEFT: "",
    TabPropertySet.RIGHT: "tqr",
    TabPropertySet.CENTER: "tqc",
    TabPropertySet.DECIMAL: "tqdec",
}

TableAlignmentMap = {Table.LEFT: "trql", Table.RIGHT: "trqr", Table.CENTER: "trqc"}

CellAlignmentMap = {
    Cell.ALIGN_TOP: "",  # clvertalt
    Cell.ALIGN_CENTER: "clvertalc",
    Cell.ALIGN_BOTTOM: "clvertalb",
}

CellFlowMap = {
    Cell.FLOW_LR_TB: "",  # cltxlrtb, Text in a cell flows from left to right and top to bottom (default)
    Cell.FLOW_RL_TB: "cltxtbrl",  # Text in a cell flows right to left and top to bottom
    Cell.FLOW_LR_BT: "cltxbtlr",  # Text in a cell flows left to right and bottom to top
    Cell.FLOW_VERTICAL_LR_TB: "cltxlrtbv",  # Text in a cell flows left to right and top to bottom, vertical
    Cell.FLOW_VERTICAL_TB_RL: "cltxtbrlv",
}  # Text in a cell flows top to bottom and right to left, vertical

ShadingPatternMap = {
    ShadingPropertySet.HORIZONTAL: "bghoriz",
    ShadingPropertySet.VERTICAL: "bgvert",
    ShadingPropertySet.FORWARD_DIAGONAL: "bgfdiag",
    ShadingPropertySet.BACKWARD_DIAGONAL: "bgbdiag",
    ShadingPropertySet.VERTICAL_CROSS: "bgcross",
    ShadingPropertySet.DIAGONAL_CROSS: "bgdcross",
    ShadingPropertySet.DARK_HORIZONTAL: "bgdkhoriz",
    ShadingPropertySet.DARK_VERTICAL: "bgdkvert",
    ShadingPropertySet.DARK_FORWARD_DIAGONAL: "bgdkfdiag",
    ShadingPropertySet.DARK_BACKWARD_DIAGONAL: "bgdkbdiag",
    ShadingPropertySet.DARK_VERTICAL_CROSS: "bgdkcross",
    ShadingPropertySet.DARK_DIAGONAL_CROSS: "bgdkdcross",
}

TabLeaderMap = {
    TabPropertySet.DOTS: "tldot",
    TabPropertySet.HYPHENS: "tlhyph",
    TabPropertySet.UNDERLINE: "tlul",
    TabPropertySet.THICK_LINE: "tlth",
    TabPropertySet.EQUAL_SIGN: "tleq",
}

BorderStyleMap = {
    BorderPropertySet.SINGLE: "brdrs",
    BorderPropertySet.DOUBLE: "brdrth",
    BorderPropertySet.SHADOWED: "brdrsh",
    BorderPropertySet.DOUBLED: "brdrdb",
    BorderPropertySet.DOTTED: "brdrdot",
    BorderPropertySet.DASHED: "brdrdash",
    BorderPropertySet.HAIRLINE: "brdrhair",
}

SectionBreakTypeMap = {
    Section.NONE: "sbknone",
    Section.COLUMN: "sbkcol",
    Section.PAGE: "sbkpage",
    Section.EVEN: "sbkeven",
    Section.ODD: "sbkodd",
}


class Settings(list):
    def __init__(self):
        super(Settings, self).__init__()
        self._append = super(Settings, self).append

    def append(self, value, mask=None, fallback=None):
        if (value is not 0) and value in [False, None, ""]:
            if fallback:
                self._append(self, fallback)

        else:
            if mask:
                if value is True:
                    value = mask
                else:
                    value = mask % value
            self._append(value)

    def Join(self):
        if self:
            return r"\%s" % "\\".join(self)
        return ""

    def __repr__(self):
        return self.Join()


class Renderer:
    def __init__(self, write_custom_element_callback=None):
        self.character_style_map = {}
        self.paragraph_style_map = {}
        self.WriteCustomElement = write_custom_element_callback

    #
    #   All of the Rend* Functions populate a Settings object with values
    #
    def _RendPageProperties(self, section, settings, in_section):
        #  this one is different from the others as it takes the settings from a
        if in_section:
            # paper_size_code   = 'psz%s'
            paper_width_code = "pgwsxn%s"
            paper_height_code = "pghsxn%s"
            landscape = "lndscpsxn"
            margin_suffix = "sxn"

        else:
            # paper_size_code   = 'psz%s'
            paper_width_code = "paperw%s"
            paper_height_code = "paperh%s"
            landscape = "landscape"
            margin_suffix = ""

        # settings.append( section.Paper.Code,   paper_size_code  )
        settings.append(section.Paper.Width, paper_width_code)
        settings.append(section.Paper.Height, paper_height_code)

        if section.Landscape:
            settings.append(landscape)

        if section.FirstPageNumber:
            settings.append(section.FirstPageNumber, "pgnstarts%s")
            settings.append("pgnrestart")

        self._RendMarginsPropertySet(section.Margins, settings, margin_suffix)

    def _RendShadingPropertySet(self, shading_props, settings, prefix=""):
        if not shading_props:
            return

        settings.append(shading_props.Shading, prefix + "shading%s")
        settings.append(ShadingPatternMap.get(shading_props.Pattern, False))

        settings.append(
            self._colour_map.get(shading_props.Foreground, False), prefix + "cfpat%s"
        )
        settings.append(
            self._colour_map.get(shading_props.Background, False), prefix + "cbpat%s"
        )

    def _RendBorderPropertySet(self, edge_props, settings):
        settings.append(BorderStyleMap[edge_props.Style])
        settings.append(edge_props.Width, "brdrw%s")
        settings.append(self._colour_map.get(edge_props.Colour, False), "brdrcf%s")
        settings.append(edge_props.Spacing or False, "brsp%s")

    def _RendFramePropertySet(self, frame_props, settings, tag_prefix=""):
        if not frame_props:
            return

        if frame_props.Top:
            settings.append(tag_prefix + "brdrt")
            self._RendBorderPropertySet(frame_props.Top, settings)

        if frame_props.Left:
            settings.append(tag_prefix + "brdrl")
            self._RendBorderPropertySet(frame_props.Left, settings)

        if frame_props.Bottom:
            settings.append(tag_prefix + "brdrb")
            self._RendBorderPropertySet(frame_props.Bottom, settings)

        if frame_props.Right:
            settings.append(tag_prefix + "brdrr")
            self._RendBorderPropertySet(frame_props.Right, settings)

    def _RendMarginsPropertySet(self, margin_props, settings, suffix=""):
        if not margin_props:
            return

        settings.append(margin_props.Top, "margt" + suffix + "%s")
        settings.append(margin_props.Left, "margl" + suffix + "%s")
        settings.append(margin_props.Bottom, "margb" + suffix + "%s")
        settings.append(margin_props.Right, "margr" + suffix + "%s")

    def _RendParagraphPropertySet(self, paragraph_props, settings):
        if not paragraph_props:
            return
        settings.append(ParagraphAlignmentMap[paragraph_props.Alignment])

        settings.append(paragraph_props.SpaceBefore, "sb%s")
        settings.append(paragraph_props.SpaceAfter, "sa%s")

        #   then we have to find out all of the tabs
        width = 0
        for tab in paragraph_props.Tabs:
            settings.append(TabAlignmentMap[tab.Alignment])
            settings.append(TabLeaderMap.get(tab.Leader, ""))

            width += tab.Width or DEFAULT_TAB_WIDTH
            settings.append("tx%s" % width)

        settings.append(paragraph_props.PageBreakBefore, "pagebb")

        settings.append(paragraph_props.FirstLineIndent, "fi%s")
        settings.append(paragraph_props.LeftIndent, "li%s")
        settings.append(paragraph_props.RightIndent, "ri%s")

        if paragraph_props.SpaceBetweenLines:
            if paragraph_props.SpaceBetweenLines < 0:
                settings.append(paragraph_props.SpaceBetweenLines, r"sl%s\slmult0")
            else:
                settings.append(paragraph_props.SpaceBetweenLines, r"sl%s\slmult1")

    def _RendTextPropertySet(self, text_props, settings):
        if not text_props:
            return

        if text_props.Expansion:
            settings.append(text_props.Expansion, "expndtw%s")

        settings.append(text_props.Bold, "b")
        settings.append(text_props.Italic, "i")
        settings.append(text_props.Underline, "ul")
        settings.append(text_props.DottedUnderline, "uld")
        settings.append(text_props.DoubleUnderline, "uldb")
        settings.append(text_props.WordUnderline, "ulw")

        settings.append(self._font_map.get(text_props.Font, False), "f%s")
        settings.append(text_props.Size, "fs%s")
        settings.append(self._colour_map.get(text_props.Colour, False), "cf%s")

        if text_props.Frame:
            frame = text_props.Frame
            settings.append("chbrdr")
            settings.append(BorderStyleMap[frame.Style])
            settings.append(frame.Width, "brdrw%s")
            settings.append(self._colour_map.get(frame.Colour, False), "brdrcf%s")

    #
    #   All of the Write* functions will write to the internal file object
    #
    #   the _ ones probably don't need to be used by anybody outside
    #   but the other ones like WriteTextElement could be used in the Custom
    #   callback.
    def Write(self, document, fout):
        #  write all of the standard stuff based upon the first document
        self._doc = document
        self._fout = fout
        self._WriteDocument()
        self._WriteColours()
        self._WriteFonts()
        self._WriteStyleSheet()

        settings = Settings()
        self._RendPageProperties(self._doc.Sections[0], settings, in_section=False)
        self._write(repr(settings))

        #  handle the simplest case first, we don't need to do anymore mucking around
        #  with section headers, etc we can just rip the document out
        if len(document.Sections) == 1:
            self._WriteSection(document.Sections[0], is_first=True, add_header=False)

        else:
            for section_idx, section in enumerate(document.Sections):
                is_first = section_idx == 0
                add_header = True
                self._WriteSection(section, is_first, add_header)

        self._write("}")

        del self._fout, self._doc, self._CurrentStyle

    def _write(self, data, *params):
        # ----------------------------------
        # begin modification
        # by Herbert Weinhandl
        # to convert accented characters
        # to their rtf-compatible form
        # for c in range( 128, 256 ) :
        #   data = data.replace( chr(c), "\'%x" % c)
        # end modification
        #
        #  This isn't the right place for this as it is going to do
        #  this loop for all sorts of writes, including settings, control codes, etc.
        #
        #  I will create a def _WriteText (or something) method that is used when the
        #  actual string that is to be viewed in the document is written, this can then
        #  do the final accented character check.
        #
        #  I left it here so that I remember to do the right thing when I have time
        # ----------------------------------

        if params:
            data = data % params
        self._fout.write(data)

    def _WriteDocument(self):
        settings = Settings()

        assert Languages.IsValid(self._doc.DefaultLanguage)
        assert ViewKind.IsValid(self._doc.ViewKind)
        assert ViewZoomKind.IsValid(self._doc.ViewZoomKind)
        assert ViewScale.IsValid(self._doc.ViewScale)

        settings.append(self._doc.DefaultLanguage, "deflang%s")
        settings.append(self._doc.ViewKind, "viewkind%s")
        settings.append(self._doc.ViewZoomKind, "viewzk%s")
        settings.append(self._doc.ViewScale, "viewscale%s")

        self._write("{\\rtf1\\ansi\\ansicpg1252\\deff0%s\n" % settings)

    def _WriteColours(self):
        self._write(r"{\colortbl ;")

        self._colour_map = {}
        offset = 0
        for colour in self._doc.StyleSheet.Colours:
            self._write(
                r"\red%s\green%s\blue%s;", colour.Red, colour.Green, colour.Blue
            )
            self._colour_map[colour] = offset + 1
            offset += 1
        self._write("}\n")

    def _WriteFonts(self):
        self._write(r"{\fonttbl")

        self._font_map = {}
        offset = 0
        for font in self._doc.StyleSheet.Fonts:
            pitch = ""
            panose = ""
            alternate = ""
            if font.Pitch:
                pitch = r"\fprq%s" % font.Pitch
            if font.Panose:
                panose = r"{\*\panose %s}" % font.Panose
            if font.Alternate:
                alternate = r"{\*\falt %s}" % font.Alternate.Name

            self._write(
                r"{\f%s\f%s%s\fcharset%s%s %s%s;}",
                offset,
                font.Family,
                pitch,
                font.CharacterSet,
                panose,
                font.Name,
                alternate,
            )

            self._font_map[font] = offset
            offset += 1

        self._write("}\n")

    def _WriteStyleSheet(self):
        self._write(r"{\stylesheet")

        #   TO DO: character styles, does anybody actually use them?

        offset_map = {}
        for idx, style in enumerate(self._doc.StyleSheet.ParagraphStyles):
            offset_map[style] = idx

        #   paragraph styles
        self.paragraph_style_map = {}
        for idx, style in enumerate(self._doc.StyleSheet.ParagraphStyles):
            if idx == 0:
                default = style
            else:
                self._write("\n")

            settings = Settings()

            #   paragraph properties
            self._RendParagraphPropertySet(style.ParagraphPropertySet, settings)
            self._RendFramePropertySet(style.FramePropertySet, settings)
            self._RendShadingPropertySet(style.ShadingPropertySet, settings)

            #   text properties
            self._RendTextPropertySet(style.TextStyle.TextPropertySet, settings)
            self._RendShadingPropertySet(style.TextStyle.ShadingPropertySet, settings)

            #   have to take
            based_on = "\\sbasedon%s" % offset_map.get(style.BasedOn, 0)
            next = "\\snext%s" % offset_map.get(style.Next, 0)

            inln = "\\s%s%s" % (idx, settings)
            self._write("{%s%s%s %s;}", inln, based_on, next, style.Name)

            self.paragraph_style_map[style] = inln

        #   if now style is specified for the first paragraph to be written, this one
        #   will be used
        self._CurrentStyle = self.paragraph_style_map[default]

        self._write("}\n")

    def _WriteSection(self, section, is_first, add_header):
        def WriteHF(hf, rtfword):
            # if not hf : return

            #  if we don't have anything in the header/footer then include
            #  a blank paragraph, this stops it from picking up the header/footer
            #  from the previous section
            # if not hf :   hf = [ Paragraph( '' ) ]
            if not hf:
                hf = []

            self._write("{\\%s" % rtfword)
            self._WriteElements(hf)
            self._write("}\n")

        settings = Settings()

        if not is_first:
            #  we need to finish off the preceding section
            #  and reset all of our defaults back to standard
            settings.append("sect")

        #  reset to our defaults
        settings.append("sectd")

        if add_header:
            settings.append(SectionBreakTypeMap[section.BreakType])
            self._RendPageProperties(section, settings, in_section=True)

        settings.append(section.HeaderY, "headery%s")
        settings.append(section.FooterY, "footery%s")

        #  write all of these out now as we need to do a write elements in the
        #  next section
        self._write(repr(settings))

        #   finally after all that has settled down we can do the
        #   headers and footers
        if section.FirstHeader or section.FirstFooter:
            #  include the titlepg flag if the first page has a special format
            self._write(r"\titlepg")
            WriteHF(section.FirstHeader, "headerf")
            WriteHF(section.FirstFooter, "footerf")

        WriteHF(section.Header, "header")
        WriteHF(section.Footer, "footer")

        #   and at last the contents of the section that actually appear on the page
        self._WriteElements(section)

    def _WriteElements(self, elements):
        new_line = ""
        for element in elements:
            self._write(new_line)
            new_line = "\n"

            clss = element.__class__

            if clss == Paragraph:
                self.WriteParagraphElement(element)

            elif clss == Table:
                self.WriteTableElement(element)

            elif ininstance(element, StringType):
                self.WriteParagraphElement(Paragraph(element))

            elif clss in [RawCode, Image]:
                self.WriteRawCode(element)

            # elif clss == List  :
            #   self._HandleListElement( element )

            elif self.WriteCustomElement:
                self.WriteCustomElement(self, element)

            else:
                raise Exception("Don't know how to handle elements of type %s" % clss)

    def WriteParagraphElement(
        self,
        paragraph_elem,
        tag_prefix="",
        tag_suffix=r"\par",
        opening="{",
        closing="}",
    ):
        #   the tag_prefix and the tag_suffix take care of paragraphs in tables.  A
        #   paragraph in a table requires and extra tag at the front (intbl) and we
        #   don't want the ending tag everytime.  We want it for all paragraphs but
        #   the last.

        overrides = Settings()
        self._RendParagraphPropertySet(paragraph_elem.Properties, overrides)
        self._RendFramePropertySet(paragraph_elem.Frame, overrides)
        self._RendShadingPropertySet(paragraph_elem.Shading, overrides)

        #   when writing the RTF the style is carried from the previous paragraph to the next,
        #   so if the currently written paragraph has a style then make it the current one,
        #   otherwise leave it as it was
        self._CurrentStyle = self.paragraph_style_map.get(
            paragraph_elem.Style, self._CurrentStyle
        )

        self._write(
            r"%s\pard\plain%s %s%s "
            % (opening, tag_prefix, self._CurrentStyle, overrides)
        )

        for element in paragraph_elem:
            if isinstance(element, StringType):
                self._write(element)

            elif isinstance(element, RawCode):
                self._write(element.Data)

            elif isinstance(element, Text):
                self.WriteTextElement(element)

            elif isinstance(element, Inline):
                self.WriteInlineElement(element)

            elif element == TAB:
                self._write(r"\tab ")

            elif element == LINE:
                self._write(r"\line ")

            elif self.WriteCustomElement:
                self.WriteCustomElement(self, element)

            else:
                raise Exception("Don't know how to handle %s" % element)

        self._write(tag_suffix + closing)

    def WriteRawCode(self, raw_elem):
        self._write(raw_elem.Data)

    def WriteTextElement(self, text_elem):
        overrides = Settings()

        self._RendTextPropertySet(text_elem.Properties, overrides)
        self._RendShadingPropertySet(text_elem.Shading, overrides, "ch")

        #   write the wrapper and then let the custom handler have a go
        if overrides:
            self._write("{%s " % repr(overrides))

        #   if the data is just a string then we can now write it
        if isinstance(text_elem.Data, StringType):
            self._write(text_elem.Data or "")

        elif text_elem.Data == TAB:
            self._write(r"\tab ")

        else:
            self.WriteCustomElement(self, text_elem.Data)

        if overrides:
            self._write("}")

    def WriteInlineElement(self, inline_elem):
        overrides = Settings()

        self._RendTextPropertySet(inline_elem.Properties, overrides)
        self._RendShadingPropertySet(inline_elem.Shading, overrides, "ch")

        #   write the wrapper and then let the custom handler have a go
        if overrides:
            self._write("{%s " % repr(overrides))

        for element in inline_elem:
            #   if the data is just a string then we can now write it
            if isinstance(element, StringType):
                self._write(element)

            elif isinstance(element, RawCode):
                self._write(element.Data)

            elif element == TAB:
                self._write(r"\tab ")

            elif element == LINE:
                self._write(r"\line ")

            else:
                self.WriteCustomElement(self, element)

        if overrides:
            self._write("}")

    def WriteText(self, text):
        self._write(text or "")

    def WriteTableElement(self, table_elem):
        vmerge = [False] * table_elem.ColumnCount
        for height, cells in table_elem.Rows:
            #   calculate the right hand edge of the cells taking into account the spans
            offset = table_elem.LeftOffset or 0
            cellx = []
            cell_idx = 0
            for cell in cells:
                cellx.append(
                    offset + sum(table_elem.ColumnWidths[: cell_idx + cell.Span])
                )
                cell_idx += cell.Span

            self._write(r"{\trowd")

            settings = Settings()

            #   the spec says that this value is mandatory and I think that 108 is the default value
            #   so I'll take care of it here
            settings.append(table_elem.GapBetweenCells or 108, "trgaph%s")
            settings.append(TableAlignmentMap[table_elem.Alignment])
            settings.append(height, "trrh%s")
            settings.append(table_elem.LeftOffset, "trleft%s")

            width = table_elem.LeftOffset or 0
            for idx, cell in enumerate(cells):
                self._RendFramePropertySet(cell.Frame, settings, "cl")

                #  cells don't have margins so I don't know why I was doing this
                #  I think it might have an affect in some versions of some WPs.
                # self._RendMarginsPropertySet( cell.Margins, settings, 'cl' )

                #  if we are starting to merge or if this one is the first in what is
                #  probably a series of merges then start the vertical merging
                if cell.StartVerticalMerge or (cell.VerticalMerge and not vmerge[idx]):
                    settings.append("clvmgf")
                    vmerge[idx] = True

                elif cell.VerticalMerge:
                    # ..continuing a merge
                    settings.append("clvmrg")

                else:
                    # ..no merging going on so make sure that it is off
                    vmerge[idx] = False

                #  for any cell in the next row that is covered by this span we
                #  need to run off the vertical merging as we don't want them
                #  merging up into this spanned cell
                for vmerge_idx in range(idx + 1, idx + cell.Span - 1):
                    vmerge[vmerge_idx] = False

                settings.append(CellAlignmentMap[cell.Alignment])
                settings.append(CellFlowMap[cell.Flow])

                #  this terminates the definition of a cell and represents the right most edge of the cell from the left margin
                settings.append(cellx[idx], "cellx%s")

            self._write(repr(settings))

            for cell in cells:
                if len(cell):
                    last_idx = len(cell) - 1
                    for element_idx, element in enumerate(cell):
                        #   wrap plain strings in paragraph tags
                        if isinstance(element, StringType):
                            element = Paragraph(element)

                        #   don't forget the prefix or else word crashes and does all sorts of strange things
                        if element_idx == last_idx:
                            self.WriteParagraphElement(
                                element,
                                tag_prefix=r"\intbl",
                                tag_suffix="",
                                opening="",
                                closing="",
                            )

                        else:
                            self.WriteParagraphElement(
                                element, tag_prefix=r"\intbl", opening="", closing=""
                            )

                    self._write(r"\cell")

                else:
                    self._write(r"\pard\intbl\cell")

            self._write("\\row}\n")
